package org.chipu.jnids.sancp;

import java.io.File;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.chipu.jnids.Jnids;
import org.chipu.jnids.beans.*;
import org.chipu.jnids.controller.DBLayer;
import org.chipu.shared.util.singletons.LoggingUtils;

public final class ProfileCreatorFiles extends ProfileCreator {
	private List<StatFileHelper> statFiles;
	/**
	 * Necessary because we calculate in and out at the same time, but the parent class makes 2 separate calls
	 */
	private transient Set<Flow> connectionsOut;

	public ProfileCreatorFiles(DBLayer db, SancpManager sancp, Logger logger) {
		super(db, sancp);
		setLogger(logger);
	}

	/** @return the ordered list by startTime */
	public static List<StatFileHelper> getSancpStatsLogs(Iterable<File> logFiles) {
		ArrayList<StatFileHelper> l = new ArrayList<StatFileHelper>();
		for(File f : logFiles)
			try {
				l.add(new StatFileHelper(f));
			} catch(Exception ex) {
				LoggingUtils.getLogger().log(Level.SEVERE, "Problem reading SANCP stats file: " + f, ex);
			}
		Collections.sort(l, new Comparator<StatFileHelper>() {
			@Override
			public int compare(StatFileHelper o1, StatFileHelper o2) {
				return o1.startTimeInSeconds - o2.startTimeInSeconds;
			}
		});
		return l;
	}

	@Override
	void process(final Profile p) throws Exception {
		if (!sancp.preprocessProfileFiles())
			throw new IllegalStateException("Not enough data to create the initial profile");
		long t = System.currentTimeMillis();
		statFiles = getSancpStatsLogs(sancp.getSancpStatsLogFiles());
		if (statFiles.isEmpty())
			throw new IllegalStateException("Not enough data to create the initial profile (2)");
		LoggingUtils.getLogger().finest(
			"Ordered " + statFiles.size() + " stat logs in " + (System.currentTimeMillis() - t) + " ms");
		super.process(p);
	}

	@Override
	protected int getInitTime() {
		return statFiles.get(0).startTimeInSeconds + timePrecision / 2;
	}

	@Override
	protected int getEndTime() {
		return statFiles.get(statFiles.size() - 1).stopTimeInSeconds;
	}

	@Override
	protected Set<Flow> getConnectionsInAt(int time) throws Exception {
		Set<Flow> connectionsIn = new HashSet<Flow>(snapshotConnectionSize);
		connectionsOut = new HashSet<Flow>(snapshotConnectionSize);
		loadConnectionsAt(time, connectionsIn);
		return connectionsIn;
	}

	/** Must be called just after #getConnectionsInAt() */
	@Override
	protected Set<Flow> getConnectionsOutAt(int time) {
		return connectionsOut;
	}

	private List<StatFileHelper> getRelevantStatFiles(int timeInSeconds) {
		ArrayList<StatFileHelper> list = new ArrayList<StatFileHelper>();
		boolean b = false;
		for(StatFileHelper sfh : statFiles)
			if (sfh.startTimeInSeconds < timeInSeconds && sfh.stopTimeInSeconds > timeInSeconds) {
				list.add(sfh);
				b = true;
			} else if (b)
				break;
		return list;
	}

	// ----- 1 way Reading all the flows and processing the necessary ones

	/**
	 * It gets all the flows from the stat files
	 * @param connectionsIn Loads there the incoming flows that were active at <code>time</code>
	 * @param connectionsOut Loads there the outgoing flows that were active at <code>time</code>
	 */
	// PERFORMANCE (60 % of time at profile creation)
	private void loadConnectionsAt(int timeInSeconds, Set<Flow> connectionsIn) throws Exception {
		ArrayList<Flow> relevantFlows = new ArrayList<Flow>();
		for(StatFileHelper sfh : getRelevantStatFiles(timeInSeconds))
			relevantFlows.addAll(sfh.getFlows());
		if (relevantFlows.isEmpty())
			return;
		// much faster than enhanced loop
		for(int i = getFirstFlowIndex(timeInSeconds, relevantFlows) + 1; i < getLastFlowIndex(timeInSeconds,
			relevantFlows) + 1; i++) {
			// much faster than enhanced loop
			Flow f = relevantFlows.get(i);
			// this check order is the optimum
			boolean fromLAN = f.isFromInside();
			boolean toLAN = f.isToInside();
			if (fromLAN && toLAN || !f.wasActivedAt(timeInSeconds)) {/**/
			} else if (fromLAN)
				connectionsIn.add(f);
			else if (toLAN)
				connectionsOut.add(f);
			else
				throw new IllegalStateException(
					"The package neither comes from LAN nor is sent to LAN. (LAN="
						+ Jnids.localNetwork
						+ "): "
						+ f
						+ "\n\tAnalyzing data from a different network. Please set manually the correct network");
		}
	}

	// SEE use Collections.iteratorBinarySearch
	/**
	 * It uses a binary search algorithm
	 * @param orderedList Ideally ordered by stop time. In practice is ordered by start time, and the result
	 *        is only approximate, but it is enough
	 * @return The last flow that stops before that time. (It should not be included in the list)
	 */
	static int getFirstFlowIndex(int timeInSeconds, List<Flow> orderedList) {
		if (orderedList.isEmpty())
			return -1;
		int low = 0;
		int high = orderedList.size() - 1;
		// if last item was greater or less than the desired item
		Boolean last = null;
		while(low <= high) {
			// unsigned right shift operator (x/2)
			int mid = (low + high) >>> 1;
			int cmp = timeInSeconds - orderedList.get(mid).stopTimeInSeconds;
			// System.err.println(Utils.toDate(orderedList.get(mid).stopTimeInSeconds) + "   " + cmp +
			// "  " + low + "  " + mid + "  " + high);
			if (cmp > 0) {
				if (low == mid && last && low == high)
					break;
				else if (low == mid && last)
					low = mid + 1;
				else
					low = mid;
				last = Boolean.TRUE;
			} else if (cmp < 0) {
				high = mid - 1;
				last = Boolean.FALSE;
			} else
				// key found
				return mid;
		}
		assert low >= 0 && low < orderedList.size() : "Low=" + low + ". Ordered list size="
			+ orderedList.size();
		return Math.min(low, high);
	}

	/**
	 * It uses a binary search algorithm
	 * @return The last flow that starts before or at that time. (It should be included in the list)
	 */
	public static int getLastFlowIndex(int timeInSeconds, List<Flow> orderedList) {
		int low = 0;
		int high = orderedList.size() - 1;
		Boolean last = null;
		while(low <= high) {
			int mid = (low + high) >>> 1;
			int cmp = timeInSeconds - orderedList.get(mid).startTimeInSeconds;
			// System.err.println(Utils.toDate(orderedList.get(mid).startTimeInSeconds) + "    " + cmp +
			// "   " + low + "   " + mid + "   " + high);
			if (cmp > 0) {
				if (low == mid && last && low == high)
					break;
				else if (low == mid && last)
					low = mid + 1;
				else
					low = mid;
				last = Boolean.TRUE;
			} else if (cmp < 0) {
				high = mid - 1;
				last = Boolean.FALSE;
			} else
				// key found
				return mid;
		}
		assert low >= 0 && low < orderedList.size();
		return Math.min(low, high);
	}

	// ----- 2 way. Reading only the necessary flows

	private void loadConnectionsAt2(int timeInSeconds, Set<Flow> connectionsIn, Set<Flow> connectionsOut)
		throws Exception {
		List<Flow> relevantFlows = getRelevantFlows(timeInSeconds);
		for(int i = 0; i < relevantFlows.size(); i++) {
			Flow f = relevantFlows.get(i);
			boolean fromLAN = f.isFromInside();
			boolean toLAN = f.isToInside();
			if (fromLAN && toLAN || !f.wasActivedAt(timeInSeconds)) {/**/
			} else if (fromLAN)
				connectionsIn.add(f);
			else if (toLAN)
				connectionsOut.add(f);
			else
				throw new IllegalStateException(
					"The package neither comes from LAN nor is sent to LAN. (LAN="
						+ Jnids.localNetwork
						+ "): "
						+ f
						+ "\n\tAnalyzing data from a different network. Please set manually the correct network");
		}
	}

	private List<Flow> getRelevantFlows(int timeInSeconds) throws Exception {
		ArrayList<Flow> list = new ArrayList<Flow>();
		int i = 0;
		List<StatFileHelper> l = getRelevantStatFiles(timeInSeconds);
		for(StatFileHelper sfh : l) {
			List<Flow> flows = sfh.getFlows();

			// for(Flow f : flows)
			// if (f.wasActivedAt(timeInSeconds))
			// list.add(f);

			if (i == 0) {
				int x = getFirstFlowIndex(timeInSeconds, flows);
				if (x == -1)
					list.addAll(flows);
				else
					for(int ii = x; ii < flows.size(); ii++)
						list.add(flows.get(ii));
			} else if (i == l.size())
				for(int ii = 0; ii < getLastFlowIndex(timeInSeconds, flows); ii++)
					list.add(flows.get(ii));
			else
				list.addAll(flows);
			i++;
		}
		return list;
	}
}